# stoker_ldap:  LDAP interface for stoker
#
# Author:   Michael Fenn (mfenn@cs.clemson.edu)
#           Mike Murphy (mamurph@cs.clemson.edu)
# Revision: 16 September 2008
#
#   Copyright 2007-2008 Clemson University
#
#   Licensed under the Apache License, Version 2.0 (the "License");
#   you may not use this file except in compliance with the License.
#   You may obtain a copy of the License at
#
#       http://www.apache.org/licenses/LICENSE-2.0
#
#   Unless required by applicable law or agreed to in writing, software
#   distributed under the License is distributed on an "AS IS" BASIS,
#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#   See the License for the specific language governing permissions and
#   limitations under the License.
#
# This material is based upon work supported under a National Science
# Foundation Graduate Research Fellowship. Any opinions, findings,
# conclusions or recommendations expressed in this publication are those of
# the author(s) and do not necessarily reflect the views of the National
# Science Foundation.

# TODO: need better error handling (not going to make it into 0.90)

import ldap_query, sys, stoker_base
from stoker_defines import CFG_LDAP_URI, CFG_LDAP_TLS, CFG_LDAP_BIND_DN, \
      CFG_LDAP_BIND_CRED, CFG_LDAP_BASE_DN, CFG_LDAP_LIMIT, DFLT_LDAP_URI, \
      DFLT_LDAP_TLS, DFLT_LDAP_BIND_DN, DFLT_LDAP_BIND_CRED, \
      DFLT_LDAP_BASE_DN, DFLT_LDAP_LIMIT, NIS_HOSTNAME_ATTR, NIS_IP_ATTR, \
      NIS_MAC_ATTR, NIS_HOSTNAME_INDEX, NIS_IP_INDEX, NIS_MAC_INDEX, \
      EXIT_ERROR_LDAP, HOSTNAME_KEY, IPV4_KEY, MAC_KEY
#
from stoker_base import pr_warning, pr_error
#
from stoker_messages import MSG_LDAP_ERROR, MSG_DUPLICATE_GROUP, \
                            MSG_DUPLICATE_MACHINE
#


class LDAPMachines(stoker_base.MachineSourceBase):
   def __init__(self, stoker_config):
      stoker_base.MachineSourceBase.__init__(self, stoker_config)
      uri = self.config.getParam(CFG_LDAP_URI, DFLT_LDAP_URI)
      tls = self.config.getParam(CFG_LDAP_TLS, DFLT_LDAP_TLS)
      bind_dn = self.config.getParam(CFG_LDAP_BIND_DN, DFLT_LDAP_BIND_DN)
      bind_cred = self.config.getParam(CFG_LDAP_BIND_CRED, DFLT_LDAP_BIND_CRED)
      base_dn = self.config.getParam(CFG_LDAP_BASE_DN, DFLT_LDAP_BASE_DN)
      limit = self.config.getParam(CFG_LDAP_LIMIT, DFLT_LDAP_LIMIT)
      #
      try:
         self.connection = ldap_query.LDAPQuery(uri, tls, bind_dn, bind_cred)
         self.tree = self.connection.query(base_dn, limit)
      except Exception, message:
         pr_error([MSG_LDAP_ERROR, message], EXIT_ERROR_LDAP)
      #
      self.groups = {}
      depth_first_flatten(self.tree, self.groups, base_dn)
      self.CLI_NAME = 'ldap'
   def resolveTargets(self, targets):
      return resolve_targets(targets, self.tree, self.groups)
   def getAllMachines(self):
      return list_all_machines(self.tree)
   def getAllGroups(self):
      return self.groups.keys()
#


# DFS with an added "flattening" twist
def depth_first_flatten(tree, group_dict, base_dn):
   for subtree in tree.subtrees.keys():
      depth_first_flatten(tree.subtrees[subtree], group_dict, base_dn)
   tree.flatten() # flatten children to current level
   if tree.name not in group_dict:
      if tree.name != base_dn:
         group_dict[tree.name] = tree.leaves[:] # copy
   else:
      pr_warning(MSG_DUPLICATE_GROUP % (tree.name))
   # No return value, since the work is done to the tree and group_dict
   # in place.
#

# List machines by name, DO NOT RESOLVE to IP
def list_all_machines(flat_tree):
   machines = []
   for host in flat_tree.leaves:
      hostname = host.attributes[NIS_HOSTNAME_ATTR][NIS_HOSTNAME_INDEX]
      if hostname not in machines:
         machines.append(hostname)
      else:
         pr_warning(MSG_DUPLICATE_MACHINE % (hostname))
   return machines
#

def make_match(machine):
   match = {}
   if machine.attributes.has_key(NIS_HOSTNAME_ATTR):
      match[HOSTNAME_KEY] = \
         machine.attributes[NIS_HOSTNAME_ATTR][NIS_HOSTNAME_INDEX]
   if machine.attributes.has_key(NIS_IP_ATTR):
      match[IPV4_KEY] = machine.attributes[NIS_IP_ATTR][NIS_IP_INDEX]
   if machine.attributes.has_key(NIS_MAC_ATTR):
      match[MAC_KEY] = machine.attributes[NIS_MAC_ATTR][NIS_MAC_INDEX]
   return match

# Actual target resolution
def resolve_targets(target_list, flat_tree, group_dict):
   resolved = []
   unresolved = []
   for target in target_list:
      if target in group_dict:
         for machine in group_dict[target]:
            match = make_match(machine)
            if match not in resolved:
               resolved.append(match)
      else:
         matches = get_machine_matches(target, flat_tree)
         if len(matches) == 0:
            unresolved.append(target)
         else:
            match = make_match(matches[0])
            if match not in resolved:
               resolved.append(match)
            if len(matches) > 1:
               pr_warning(MSG_DUPLICATE_MACHINE % (target))
   return (resolved, unresolved)
#

def get_machine_matches(target, flat_tree):
   matches = []
   for leaf in flat_tree.leaves:
      if target == leaf.attributes[NIS_HOSTNAME_ATTR][NIS_HOSTNAME_INDEX]:
         matches.append(leaf)
   return matches



# Test code
if __name__ == '__main__':
   import scf2
   stoker_config = scf2.ConfigHandler()
   stoker_config.parseConfigFile('stoker.conf')
   inst = LDAPMachines(stoker_config)
   print inst.getAllMachines()
